2.2 链表
链表是一种常见的线性数据结构,与数组不同的是,链表中的元素并不存储在连续的内存空间中,而是通过指针连接在一起。
链表非常适合需要频繁插入、删除操作的场景,因为它不需要像数组那样频繁移动数据。
本节代码存放目录为 lesson3
概念及原理
单向链表
单向链表是一种由多个节点组成的线性数据结构。每个节点包含两个部分:
数据部分:存储节点中的实际数据。
指针部分:存储指向下一个节点的地址。
链表中的节点通过指针彼此连接,最后一个节点指向 nil
,表示链表的结束。
单向链表的结构示意图如下所示:
+-------+ +-------+ +-------+ +-------+
| 1 | -> | 2 | -> | 3 | -> | 4 | -> nil
+-------+ +-------+ +-------+ +-------+
从上面的示意图中我们可以看出,单向链表与数组其实有一些相似之处。
数组是线性结构,所有元素存储在连续的内存空间中,每个元素只包含一个值,因此我们可以通过索引快速访问任意位置的元素,时间复杂度为 O(1)。
单向链表也是线性结构,但它的元素(节点)在内存中不连续存储。每个节点除了存储数据外,还包含一个指向下一个节点的指针。查询某个节点时,我们需要从头节点开始,逐个访问每个节点,直到找到目标元素,时间复杂度为 O(n)。
单向链表的优缺点如下:
优点:插入和删除操作效率高,特别是在链表的头部或中部。
缺点:无法通过索引直接访问元素,需要从头开始遍历链表才能找到目标元素。
单向链表的操作如下:
插入节点:在链表的头部或尾部插入节点,或者在中间插入节点。
删除节点:根据值或位置删除节点。
遍历链表:只能从头节点开始,逐个访问链表中的每个节点。
双向链表
双向链表是一种与单向链表类似的链表结构,但它的每个节点都有两个指针:
前驱指针:指向前一个节点。
后继指针:指向后一个节点。
双向链表可以从任意节点向前或向后遍历,操作更加灵活。
双向链表的结构示意图如下:
nil <- +-------+ <-> +-------+ <-> +-------+ <-> +-------+ -> nil
| 1 | | 2 | | 3 | | 4 |
+-------+ +-------+ +-------+ +-------+
从上面的示意图我们可以看出,双向链表与单向链表最大的区别就是:双向不仅指向前一个元素,还指向后一个元素。
也就是说双向链表可以从后往前查找,也可以从前往后查找,而单向链表则只能从先往后去查找数据。
双向链表的优缺点如下:
优点:可以向前或向后遍历链表,插入、删除节点更加灵活。
缺点:相比单向链表,双向链表需要更多的内存来存储两个指针。
双向链表的操作如下:
插入节点:可以在链表的任何位置插入节点,前后指针都需要更新。
删除节点:根据值或位置删除节点,同时更新前驱和后继指针。
遍历链表:可以从任意节点向前或向后遍历链表。
循环链表
循环链表是一种特殊的链表结构,它的最后一个节点的指针指向链表的头节点,形成一个环,而不是指向 nil
。
循环链表可以是单向的,也可以是双向的。
循环链表的结构示意图如下:
- 单向循环链表:
+-------+ +-------+ +-------+ +-------+
| 1 | -> | 2 | -> | 3 | -> | 4 | -> (回到 1)
+-------+ +-------+ +-------+ +-------+
^ |
|--------------------------------------------|
完整结构:
+-------+ +-------+
| 1 | ----> | 2 |
+-------+ +-------+
^ |
| v
+-------+ +-------+
| 4 | <---- | 3 |
+-------+ +-------+
从示意图我们可以看出,单向循环链表与单向链表最大的区别就是:单向循环链表的最后一个元素还会指向第一个元素。
- 双向循环链表:
+-------+ <-> +-------+ <-> +-------+ <-> +-------+
| 1 | | 2 | | 3 | | 4 |
+-------+ +-------+ +-------+ +-------+
^ ^
|------------------------------------------|
完整结构:
+-------+ +-------+
| 1 | <--> | 2 |
+-------+ +-------+
^ ^
| |
v v
+-------+ +-------+
| 4 | <--> | 3 |
+-------+ +-------+
从示意图我们可以看出,双向循环链表与单向链表最大的却别就是:双向循环链表的最后一个元素还会与第一个元素互相指向。
总的来说循环链表就像是我们自行车的链条一样的,通过每一个扣的连接,最终形成了一个循环的圈套。
循环链表的优缺点如下:
优点:循环链表适合处理循环数据流的问题,头尾相连,能够遍历链表的每个节点而无需担心终点。
缺点:在处理过程中不容易判断链表结束,必须小心避免死循环。
循环链表的操作如下:
插入节点:可以在头部或尾部插入节点,尾部节点的指针需要重新指向头节点。
删除节点:根据值或位置删除节点,更新前驱和后继节点的指针。
遍历链表:从任意节点开始遍历,循环遍历链表中的所有节点。
Go语言的实现
单向链表的实现
// Node 定义单向列表的节点结构
type Node struct {
// 数据
data int
// 指向下一个节点
next *Node
}
func appendNode(head *Node, data int) *Node {
newNode := &Node{
data: data,
}
// 第一个元素
if head == nil {
return newNode
}
current := head
// 如果当前元素是有指向的,那么使用下一个
for current.next != nil {
current = head.next
}
// 指向新的元素
current.next = newNode
return head
}
func deleteNode(head *Node, data int) *Node {
if head == nil {
return nil
}
current := head
for current.next != nil {
if current.next.data == data {
current.next = current.next.next
break
}
current = current.next
}
return head
}
func printNode(head *Node) {
current := head
// 读取所有元素展示
for current != nil {
fmt.Print(current.data, " -> ")
current = current.next
}
fmt.Println("nil")
}
func main() {
var (
head *Node
)
head = appendNode(head, 1)
head = appendNode(head, 2)
head = appendNode(head, 3)
printNode(head)
head = deleteNode(head, 2)
printNode(head)
}
执行输入如下所示:
1 -> 2 -> 3 -> nil
1 -> 3 -> nil
在上面的代码中,我们使用Go
语言实现了单向链表,其实只需要理解了它的本质,那么使用起来就会比较简单。
我们只需要关注一点,就是每一个元素都有一个指针指向,那么最终形成的就是一条铁链,一个链着下一个。
双向链表的实现
// Node 定义双向列表的节点结构
type Node struct {
// 数据
data int
// 指向上一个节点
prev *Node
// 指向下一个节点
next *Node
}
func appendNode(head *Node, data int) *Node {
newNode := &Node{
data: data,
}
if head == nil {
return newNode
}
current := head
for current.next != nil {
current = current.next
}
current.next = newNode
newNode.prev = current
return head
}
func delNode(head *Node, data int) *Node {
if head == nil {
return nil
}
current := head
for current.next != nil {
// 匹配到
if current.next.data == data {
// 更新当前的后续指向
current.next = current.next.next
if current.next.next != nil {
// 更新新的前序指向,将它们链接起来
current.next.next.prev = current
}
break
}
current = current.next
}
return head
}
func printNode(head *Node) {
current := head
for current != nil {
fmt.Printf("%d <-> ", current.data)
current = current.next
}
fmt.Println("nil")
}
func main() {
var (
head *Node
)
head = appendNode(head, 1)
head = appendNode(head, 2)
head = appendNode(head, 3)
printNode(head)
delNode(head, 2)
printNode(head)
}
结果输出如下所示:
1 <-> 2 <-> 3 <-> nil
1 <-> 3 <-> nil
双向链表的实现其实与单向是差不多的,只不过在结构体中多了prev
,也就是说我们在添加元素时需要将元素的前序指向也进行标记。
所以我们其实可以从前往后找,同时我们也可以通过遍历从后往前找,只需要知道其中的任意一个元素,都可以找到前面、后面的所有元素。
循环链表的实现
// Node 定义单向循环链表节点结构
type Node struct {
data int
// 指向下一个元素的指针节点
Next *Node
}
func appendNode(head *Node, data int) *Node {
newNode := &Node{
data: data,
}
if head == nil {
newNode.Next = newNode
return newNode
}
current := head
for current.Next != head {
current = current.Next
}
current.Next = newNode
newNode.Next = head
return head
}
func delNode(head *Node, data int) *Node {
if head == nil {
return nil
}
current := head
for current.Next != head {
if current.Next.data == data {
current.Next = current.Next.Next
break
}
current = current.Next
}
return head
}
func printNode(head *Node) {
if head == nil {
return
}
current := head
for current.Next != nil {
fmt.Printf("%d -> ", current.data)
current = current.Next
if current == head {
// 循环到了开头
break
}
}
fmt.Println("(回到头节点)")
}
func main() {
var (
head *Node
)
head = appendNode(head, 1)
head = appendNode(head, 2)
head = appendNode(head, 3)
printNode(head)
head = delNode(head, 2)
printNode(head)
}
结果输出如下所示:
1 -> 2 -> 3 -> (回到头节点)
1 -> 3 -> (回到头节点)
循环链表的实现与原本的单向、双向其实是差不多的。只需要将原本末尾节点再次链接指向到头部节点即可。
在循环链表中需要注意出现死循环,在更新节点或者增删节点时是比较容易出现这个问题的。
总的来说,链表的实现其实就是通过结构体字段指针进行指向,形成一个铁链结构。